package oracle;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Random;
/**
 * This is the oracle that knows about the board
 * and is queried for moves and information about the board
 * It puts restrictions on when the user can actually make a move,
 * and will often return null if the user is trying to do something wrong.
 * Otherwise it returns some nice data
 * The user has to increment the rounds, but not the board numbers or score
 * @author mdailey
 *
 */
public class Oracle {
	// Run information
	/** this is our run number */
	private int currRun = 0;
	/** This keeps track of the score for each run in a round */
	public double[][] scorePerRun = new double[MAX_NUMBER_BOARDS][MAX_NUMBER_RUNS];
	
	/** This is the score on the current round */
	private double currRoundScore = 0;
	/** This is the (n-1) different board, ie round number */
	private int currRound = 0;
	
	/** our random number generator, with a seed of 0 */
	private static Random generator = new Random();
	/** The maximum manhattan distance which a look around move can see */
	private static int MAX_MANHATTAN_DISTANCE = 10;
	
	/** This is the move number for the current board */
	private int currMoveNumber = 0;

	/** Max number of runs per game board */
	public static int MAX_NUMBER_RUNS = 10;
	
	/** The maximum number of different boards in the game */
	public static int MAX_NUMBER_BOARDS = 10;
	/** The maximum number of moves per round */
	public static int MAX_MOVES_PER_ROUND = 15;
	/** The number of terminal states */
	public static int NUM_TERMINAL_STATES = 3;	
	/** The number of rows in our world */
	public static int MAX_ROW ;
	public int MAX_ROW_A;
	/** The number of columns in our world */
	public static int MAX_COL;
	public int MAX_COL_A;
	
	/** The file path to where we're storing the world */
	public String pathToWorld;
	/** The board which consist of tile objects */
	private OracleTile[][] theBoard = new OracleTile[MAX_ROW][MAX_COL];
	/** This keeps track of the number of moves performed in each round */
	public int[] movesInEachRound = new int[MAX_NUMBER_BOARDS];
	/** This keeps track of the score in each round */
	public double[] scoreInEachRound = new double[MAX_NUMBER_BOARDS];
	
	/** The current tile of the user */
	public OracleTile currLocation;
	/** if the user is alive on the current round */
	public boolean isUserAlive;
	/** The penalty for not reaching a terminal state */
	public static int NOT_REACH_TERMINAL_PENALTY = -50;
	/** The penalty for killing yourself */
	public static int KILL_SELF_PENALTY = -50;
	/** The penalty for exploration */
	public static int PENALTY_EXPLORATION = -6;
	
	private StringBuilder contents = new StringBuilder();
	private BufferedReader input;
	private String line = null; //not declared within while loop
	/** where we will start out */
	private int[] startX = new int[10];
	private int[] startY = new int[10];	
	
	
	public Oracle(){

	}
	
	/**
	 * Let the user know where they are
	 * @return A tile representing where the user is
	 */
	public OracleTile getCurrentTile(){
		return currLocation.deepCopy();
	}
	
	public int getMaxRow(){
		return MAX_ROW_A;
	}
	
	public int getRunNumber(){
		return this.currRun;
	}
	
	
	public void initBoardInputs() throws Exception{
		System.out.println(this.pathToWorld+"map"+String.valueOf(currRound+1)+".txt");
		try {
			input =  new BufferedReader(
					//new FileReader(".\\src\\boards.txt"));				
					//new FileReader(".\\src\\world1\\map"+String.valueOf(curr_round_number+1)+".txt"));
			new FileReader(this.pathToWorld+"map"+String.valueOf(currRound+1)+".txt"));
		} catch (FileNotFoundException e) {
			System.err.println("Error opening the file with the boards");
			System.out.println(this.pathToWorld+"map"+String.valueOf(currRound+1)+".txt");
		}
		/* read in fields that are not the map */
		String [] temp = null;
		line = input.readLine();
		temp = line.split(",");
		line = temp[0];
		int v = Integer.valueOf(line).intValue(); 
		MAX_MOVES_PER_ROUND = v;
		line = input.readLine();
		temp = line.split(",");
		line = temp[0];
		v = Integer.valueOf(line).intValue(); 
		NOT_REACH_TERMINAL_PENALTY = v;
		KILL_SELF_PENALTY = v;
		System.out.println("================== " +  v);
//		System.out.println(this.pathToWorld+"map"+String.valueOf(curr_round_number+1)+".txt");
		
		
		
		// know where we start
		// this does not do any conversions
		for(int i=0; i<10; i++){
			line = input.readLine();
			
			temp = line.split("\"|,");
			v = Integer.valueOf(temp[1]).intValue();
			startY[i] = v;
			v = Integer.valueOf(temp[2]).intValue();
			startX[i] = v;
			System.out.println(i + " ___ " + startX[i] + "," + startY[i]);
		}
				
		/* read in the number of terminal states */
		line = input.readLine();
		temp = line.split(",");
		line = temp[0];
		v = Integer.valueOf(line).intValue(); 
		NUM_TERMINAL_STATES = v;
		int row = 0;
		int col = 0;
		int[] termX = new int[NUM_TERMINAL_STATES];
		int[] termY = new int[NUM_TERMINAL_STATES];	
		System.out.println("Term states");
		for(int i=0;i<NUM_TERMINAL_STATES;i++){
			line = input.readLine();
			temp = line.split("\"|,");
			v = Integer.valueOf(temp[1]).intValue();
			termY[i] = v;
			v = Integer.valueOf(temp[2]).intValue();
			termX[i] = v;
		}
		
		System.out.println("Matts");
		double d;
		/* now we need to find out max number of rows and columns */
		String allData = "";
		MAX_ROW = 0;
		while(true){
			line = input.readLine();
			if(line == null)
				break;
			allData = allData + "\n" + line;
			MAX_ROW++;
			temp = line.split(",");
			Oracle.MAX_COL = temp.length;
		}
		// now we know the actual size
		theBoard = new OracleTile[MAX_ROW][MAX_COL];
		// now actually do the stuff
		/* now read in the map */
		String newTemp[] = allData.split("\n");
		for(row = 0; row < MAX_ROW; row++){
			temp = newTemp[row+1].split(",");
			for(col = 0; col < MAX_COL; col++){
				d = Double.parseDouble(temp[col]);
				this.theBoard[MAX_ROW - row - 1][col] = new OracleTile(MAX_ROW - row - 1, col, d);
				if(d == -9999){
					theBoard[MAX_ROW - row - 1][col].isWall = true;
					System.out.println("Wall at " + (MAX_ROW - row - 1)+","+col + " val" +theBoard[MAX_ROW - row - 1][col].value );
				}							
			}
		}
		
		
		System.out.println(MAX_ROW + "  " + MAX_COL);
		MAX_ROW_A = MAX_ROW;
		MAX_COL_A = MAX_COL;
		
		for(int i=0;i<NUM_TERMINAL_STATES;i++){
			theBoard[termX[i]-1][termY[i] - 1].isTerminalState = true;
			System.out.println("Terminal at " + ( termX[i]-1) +"," + (termY[i]-1) + " " + theBoard[termX[i]-1][termY[i] - 1].value);
		}
		for(int i=0;i<Oracle.MAX_NUMBER_RUNS;i++){
			startX[i] = startX[i];
			startY[i] = startY[i];
			System.out.println("Starting at " + ( startX[i]-1) +"," + (startY[i]-1) + " " + theBoard[startX[i]-1][startY[i] - 1].value);

			if(startY[i]>= MAX_COL)
				System.out.println(i + "  too big is startY " + startY[i] + " and x is "+ startX[i]);
		}
	
		System.out.println("Valid");
		setLocation();
		currLocation.isVisible = true;
		currLocation.isVisited = true;
	
	}
	
	
	
	private void setLocation() {
		if(startX[0]> MAX_ROW){
			System.out.println("row is bad " + startX[0]);
		}
		if(startY[0]> MAX_COL){
			System.out.println("col is bad " + startY[0]);
		}
		currLocation = theBoard[startX[0]][startY[0]];
//		System.out.println("Starting Loc\n" + currLocation.toString());
		isUserAlive = true;
		currMoveNumber = 0;
		if(this.currMoveNumber != 0)
			this.currRoundScore = currLocation.value;
	}

	/**
	 * We have this definition of the manhattan distance
	 */
	private void initManDistance(){
		MAX_MANHATTAN_DISTANCE = (int) Math.ceil((MAX_ROW*MAX_COL)/100.0);
	}
	

	/**
	 * Gets the current round score
	 * @return The current round score
	 */
	public double getCurrRoundScore(){
		return currRoundScore;
	}
	
	/**
	 * Gets the maximum manhattan distance a look around move can take
	 * @return The maximum manhattan distance
	 */
	public static int getMaxSightRange(){
		return MAX_MANHATTAN_DISTANCE;
	}
	
	/**
	 * This performs a move for the user if its well specified (not hitting oob)
	 * It updates the moveNumber and user score, but does not increments the rounds
	 * That is left up to the user
	 * @param move_type The type of move that the user is trying to perform
	 * @return null if move not possible or not implemented, else a MoveObject
	 */
	public OracleMove makeMove(int move_type){
		/* make sure it is capable of making this move */
		if(currRound >= MAX_NUMBER_BOARDS){
			return null;
		}else if(currMoveNumber >= MAX_MOVES_PER_ROUND){
			return null;
		}else if(move_type == MoveTypes.HIT_WALL || move_type == MoveTypes.NO_MOVE){
			return null;
		}else if(currLocation.isTerminalState){ // can't move from a terminal state
			return null;
		}else if(!isUserAlive){	// can't move if you are dead
			return null;
		}
//		System.out.println("In here");
		
		// pretend this is our move object
		// we have a 20% chance of one of the other moves
		OracleMove newMove = null;
		int randInt = generator.nextInt(10) + 1;
	if(move_type >= 1 && move_type <= 4){	
		/* we might perform a random move */
		if(randInt <= 2){
			randInt = generator.nextInt(3) + 1;
			if(randInt >= move_type && move_type >= MoveTypes.UP && move_type <= MoveTypes.RIGHT){
				randInt++; // this works, have trust
			}
//			System.out.println("random and the value is " + randInt);
			newMove = performMove(randInt);
			while(newMove == null){	// may hit bounds here, so try again
				randInt = generator.nextInt(3)+1;	// try this other move, but still random
				if(randInt >= move_type && move_type >= MoveTypes.UP && move_type <= MoveTypes.RIGHT){
					randInt++; // this works, have trust
				}
				newMove = performMove(randInt);
			}
			newMove.isRandom = true;
			newMove.moveAttempted = move_type;
			newMove.randMoveAttempted = randInt;
			currLocation= theBoard[newMove.newLocTile.row][newMove.newLocTile.column];
			if(!currLocation.isWall)
				currRoundScore += currLocation.value;
			currMoveNumber++;
			checkForTerminal();
			makeNeighborWallsVisible();
			newMove.newLocTile = currLocation.deepCopy();
			return newMove;
		}
	}	
		/* not a random move */
		if(move_type <= MoveTypes.RIGHT){ /* if up,down,left,right */
			newMove = performMove(move_type);
			if(newMove == null){
				System.err.println("ERROR :: Cant perform predefined move");
				return null;	// ?? 
			}
			newMove.movePerformed = move_type;
			newMove.moveAttempted = move_type;
			if(!this.currLocation.isWall)
				this.currRoundScore += this.currLocation.value;
		}else if(move_type == MoveTypes.LOOK_AROUND){
			lookAround();
			newMove = new OracleMove();
			newMove.moveAttempted = MoveTypes.LOOK_AROUND;
			newMove.movePerformed = MoveTypes.LOOK_AROUND;
			newMove.isRandom = false;
			newMove.hitwall = false;
			newMove.newLocTile = this.currLocation.deepCopy();
			this.currMoveNumber++;
			this.currRoundScore += Oracle.PENALTY_EXPLORATION;
		}else if(move_type == MoveTypes.KILL_SELF){
			/* assuming you just get the death penalty and no other movement */
			// we actually attach this value here when we increment the round number 
			this.currRoundScore += KILL_SELF_PENALTY;
			isUserAlive = false;
			currMoveNumber++;
			newMove = new OracleMove();
			newMove.moveAttempted = MoveTypes.KILL_SELF;
			newMove.movePerformed = MoveTypes.KILL_SELF;
			newMove.newLocTile = currLocation.deepCopy();
			return newMove;
		}
		
		/* update our new current location, and see if the user may now be dead */
		if(move_type != MoveTypes.KILL_SELF && move_type != MoveTypes.LOOK_AROUND){
			currLocation= theBoard[newMove.newLocTile.row][newMove.newLocTile.column];
			currMoveNumber++;
			checkForTerminal();
		}
		makeNeighborWallsVisible();
		newMove.newLocTile = currLocation.deepCopy();
		return newMove;
	}
	
	/**
	 * For each state that we end up in, if we are neighboring any walls
	 * we make them visible
	 */
	private void makeNeighborWallsVisible(){
		int row = currLocation.row;
		int col = currLocation.column;
		if(row + 1 < MAX_ROW && theBoard[row+1][col].isWall){
			theBoard[row+1][col].isVisible = true;
		}
		if(row -1 >= 0 && theBoard[row-1][col].isWall){
			theBoard[row-1][col].isVisible = true;
		}
		if(col + 1 < MAX_COL && theBoard[row][col+1].isWall){
			theBoard[row][col+1].isVisible = true;
		}
		if(col - 1 >= 0 && theBoard[row][col-1].isWall){
			theBoard[row][col-1].isVisible = true;
		}
	}
	
	/**
	 * Perform the look around functionality
	 * This makes other states visible but does not do anything else
	 * 
	 */
	private void lookAround(){
		for(int r=0; r< MAX_ROW; r++){
			for(int c=0; c< MAX_COL; c++){
				if(currLocation.isReachableFrom(theBoard[r][c])){
					theBoard[r][c].isVisible = true;
				}
			}
		}
	}
	
	/**
	 * This sees if the user is now dead, by not reaching a terminal state and not killing self
	 */
	private void checkForTerminal() {
		if(currMoveNumber == MAX_MOVES_PER_ROUND){
			if(!currLocation.isTerminalState){
				currRoundScore += NOT_REACH_TERMINAL_PENALTY;
				isUserAlive = false;
			}
		}
	}

	/**
	 * This will actually perform the move
	 * but it does not update anything about the map except for possible
	 * newly visited or seen Tiles
	 * Assigns what move was actually performed
	 * @param move_type either up,down,left or right
	 * @return the moveObject for the move that is specified by move_type
	 */
	private OracleMove performMove(int move_type){
		// our move object
		OracleMove newMove = new OracleMove();
		/* this will actually perform the move if possible, else returns null */
		if(move_type == MoveTypes.DOWN){ 
			/* check if we bounce back */
			if(currLocation.row+1 >= this.MAX_ROW || theBoard[currLocation.row + 1][currLocation.column].isWall){
				newMove.hitwall = true;
				newMove.newLocTile = theBoard[currLocation.row ][currLocation.column].deepCopy();
				newMove.movePerformed = MoveTypes.HIT_WALL;
			}else{ // we actually move there
				newMove.hitwall = false;
				newMove.newLocTile = theBoard[currLocation.row + 1][currLocation.column].deepCopy();
				newMove.movePerformed = move_type;
				// mark that we have been to this board
				theBoard[currLocation.row + 1][currLocation.column].isVisited = true;
			}
		}else if(move_type == MoveTypes.UP){
//			System.out.println("CURRENTLY " + currLocation.toString());
			/* check if we bounce back */
			if(currLocation.row == 0 || theBoard[currLocation.row - 1][currLocation.column].isWall){
				newMove.hitwall = true;
				newMove.newLocTile = theBoard[currLocation.row ][currLocation.column].deepCopy();
				newMove.movePerformed = MoveTypes.HIT_WALL;
			}else{ // we actually move there
				newMove.hitwall = false;
				int newRow = currLocation.row - 1;
				newMove.newLocTile = theBoard[newRow][currLocation.column].deepCopy();
				newMove.movePerformed = move_type;
				// mark that we have been to this board
				theBoard[currLocation.row - 1][currLocation.column].isVisited = true;
			}
		}else if(move_type == MoveTypes.RIGHT){
			/* check if we bounce back */
			if(currLocation.column + 1 >= this.MAX_COL || theBoard[currLocation.row][currLocation.column+1].isWall){
				newMove.hitwall = true;
				newMove.newLocTile = theBoard[currLocation.row ][currLocation.column].deepCopy();
				newMove.movePerformed = MoveTypes.HIT_WALL;
			}else{ // we actually move there
				newMove.hitwall = false;
				newMove.newLocTile = theBoard[currLocation.row][currLocation.column+1].deepCopy();
				newMove.movePerformed = move_type;
				// mark that we have been to this board
				theBoard[currLocation.row][currLocation.column+1].isVisited = true;
			}
		}else if(move_type == MoveTypes.LEFT){
			/* check if we bounce back */
			if(currLocation.column == 0 || theBoard[currLocation.row][currLocation.column-1].isWall){
				newMove.hitwall = true;
				newMove.newLocTile = theBoard[currLocation.row ][currLocation.column].deepCopy();
				newMove.movePerformed = MoveTypes.HIT_WALL;
			}else{ // we actually move there
				newMove.hitwall = false;
				newMove.newLocTile = theBoard[currLocation.row][currLocation.column-1].deepCopy();
				newMove.movePerformed = move_type;
				// mark that we have been to this board
				theBoard[currLocation.row][currLocation.column-1].isVisited = true;
			}
		}
		currLocation = theBoard[newMove.newLocTile.row][newMove.newLocTile.column];
		currLocation.isVisible = true;
		currLocation.isVisited = true;
		return newMove;
	}
	
	/**
	 * Gets the current round number
	 * @return the current round number
	 */
	public int getRoundNumber(){
		return this.currRound;
	}
	
	/**
	 * Gets the current move number for this round;
	 * @return the current move number for this round
	 */
	public int getCurrMoveNumber(){
		return this.currMoveNumber;
	}
	
	/**
	 * Gets how many moves are remaining in this round
	 * @return the number of moves remaining in this round
	 */
	public int getNumMovesLeftInRound(){
		return this.MAX_MOVES_PER_ROUND - this.currMoveNumber;
	}
	
	/**
	 * Increments the round number if possible
	 * And updates the board number
	 * Be aware that old board will disappear
	 * @return true if the round is incremented
	 */
	public boolean incrementRoundNumber(){
		if(currRound >= MAX_NUMBER_BOARDS){
			return false;
		}else if(this.currRun + 1 < MAX_NUMBER_RUNS){
			System.out.println("too few runs");
			return false;
		}
		else{
			movesInEachRound[currRound] = currMoveNumber;
			int totalS = 0;
			for(int i=0; i< MAX_NUMBER_RUNS; i++){
				totalS+= this.scorePerRun[this.currRound][i];
			}
			scoreInEachRound[currRound] = totalS;
			currRound++;
			currMoveNumber = 0;
			currRoundScore = 0;
			this.isUserAlive = true;
			this.currRun = 0;
		}
		/* get the next board */
		if(currRound < MAX_NUMBER_BOARDS){
			readInNextBoard();
		}
		return true;
	}
	
	/** read in the next board */
	private void readInNextBoard() {
		try {
			initBoardInputs();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Returns the tile holding information about a position
	 * if it is visible to the user
	 * otherwise it returns null
	 * @return The tile if it is viewable by the user
	 */
	public OracleTile getTileByPosition(int row, int column){
		if(theBoard[row][column].isVisible)
			return theBoard[row][column].deepCopy();
		return null;
	}
	
	/**
	 * Gets the total score for the user
	 * @return The total score for the user
	 */
	public double getTotalScore(){
		double sum = 0;
		for(int i=0; i <MAX_NUMBER_BOARDS; i++)
			sum+= scoreInEachRound[i];
		return sum;
	}

	/**
	 * This increments the run number and saves the current score
	 */
	public boolean incrementRunNumber() {
		
		if(this.currRound + 1 == Oracle.MAX_NUMBER_BOARDS && this.currRun+1 == Oracle.MAX_NUMBER_RUNS){
			/* We need to end the game */
		//	endGameStats();	
		}
		if(this.isUserAlive){
			if(this.currLocation.isTerminalState){
	//			System.out.println(this.theBoard[currLocation.row][currLocation.column].toString());
	//			System.out.println("In terminal state  " + currLocation.row + "  " + currLocation.column + "\n\n\n");
				
			}else if(this.getNumMovesLeftInRound() == 1){
				this.currRoundScore += KILL_SELF_PENALTY;
				this.isUserAlive = false;
				return incrementRunNumber();				
			}
			else{
				System.out.println("\n\n " + this.getNumMovesLeftInRound()+ "\n" + this.currLocation.toString() +"\n\n");
				System.err.println("User cant increment run when not in terminal and not dead");
				this.currRoundScore += KILL_SELF_PENALTY;
				this.isUserAlive = false;
				return incrementRunNumber();
			}
		}else{
			//this.currRoundScore += Oracle.KILL_SELF_PENALTY;
		}
		scorePerRun[this.currRound][this.currRun] = this.currRoundScore;
	//	System.out.println("The user scores :: "+ this.curr_round_score);
		this.currRoundScore = 0;
		this.currRun++;
	//	System.out.println("new run number is " + this.curr_run_number);
		if(currRun != Oracle.MAX_NUMBER_RUNS){
			this.currLocation= this.theBoard[startX[currRun]][this.startY[currRun]];
		}
		this.isUserAlive = true;
		this.currMoveNumber = 0;
		makeNeighborWallsVisible();
	//	System.out.println("Are we playable " + this.isPlayable());
	//	if(currLocation.isTerminalState)
	//		System.out.println("We are in a terminal state");
		return true;
	}
	
	
	/**
	 * 
	 * @return True if the agent can make a move
	 */
	public boolean isPlayable(){
		if(this.currMoveNumber + 1<= Oracle.MAX_MOVES_PER_ROUND && this.isUserAlive && !this.currLocation.isTerminalState){
			return true;
		}
		return false;
	}
	
	
	/**
	 * Print out scoring information
	 */
	public void endGameStats(){
		int sum = 0;
		for(int i=0; i< this.MAX_NUMBER_BOARDS; i++){
			for(int c=0; c < this.MAX_NUMBER_RUNS; c++){
				System.out.print(scorePerRun[i][c] + ",");
				sum+= scorePerRun[i][c];
			}
			System.out.println();
		}
		System.out.println("Total score :: " + sum);
	}

	public void useGodMode() {
		for(int r=0; r< MAX_ROW; r++){
			for(int c=0; c< MAX_COL; c++){
				theBoard[r][c].isVisible = true;
			}
		}
	}
	
}
